/*
 * FileDumper.java	2008/07/28
 *
 * Copyright 2008 Web & Zen Multimedia, All rights reserved.
 * Use is subject to license terms.
 */
package it.wzm.lightweightlog;

import java.io.BufferedWriter;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.nio.channels.*;
import java.io.*;
import java.util.Calendar;

/**
 * <p>This class should be subclassed to create other file dumper, for example xml,
 *  xls...etc.<br>
 *  As it is, it just creates a text file.
 * </p>
 * <p>
 *  This is a listing of all the properties needed, all of them must be prefixed
 *  following the rule of {@link Dumper}:
 * <ul>
 *  <li>url = where the file is saved</li>
 *  <li>rotation = the type of rotation, only SIZE is allowed in this release</li>
 *  <li>size = the size when the file need to be rotated, 10000 means 10Kb</li>
 * </ul>
 * </p>
 * <p>
 *  This class use a buffered writer to dump Entry to the file. When the data are
 *  written the class first check the file existance, if not exists (at startup,
 *  or when it is rotated) then it is created.<br>
 *  The checking of the rotation happens after the dumping of all entry is done.
 * </p>
 */
class FileDumper extends Dumper {

    /**
     * needed for serialization
     */
    static final long serialVersionUID = 20080618L;

    /**
     * rotation type for a file dumper
     */
    public enum Rotation {

        SIZE((short) 0, "size"),
        DATE((short) 1, "date"),
        NONE((short) 2, "none");
        short type;  // for use in switch or whatever...type
        String name; // human readable name

        Rotation(short type, String name) {
            this.type = type;
            this.name = name.toUpperCase();
        }
        // getter for the variable above
        short getType() {
            return this.type;
        }

        String getName() {
            return this.name;
        }
    }

    /**
     * type of rotation based on date
     */
    public enum When {
    	//QUARTERLY(900000L),
    	//HALFLY(180000L),
        HOURLY(360000L),
        DAILY(8640000L),
        WEEKLY(60480000L),
        MONTHLY(1),
        YEARLY(3153600000L);
        
        long time; // conversion to millisec 
        
        When(long time) {
    		this.time = time;
    	}
    	
    	long getTime() {
    		return this.time;
    	}
    	
    }
    /**
     * The main stream for writing to file
     */
    private BufferedWriter bw;
    /**
     * the path of the file
     */
    private String url;
    /**
     * type of rotation
     */
    private Rotation rotation = Rotation.SIZE;
    /**
     * the size of the rotation
     */
    private long sizeLimit;
    /**
     * the file itself
     */
    private File f;
    /**
     * When we need to rotate
     */
    private boolean rotate;
    /**
     * When the file rotation by date occurs
     */
    private When when;
    /**
     * When to check for rotation...millisec...
     */
    private long checkRotationInterval;
    /**
     * 
     */
    private FileChannel ch;

    /**
     * Construct a FileDumper with a given set of properties
     * @param p Properties
     */
    public FileDumper(Properties p) {
        // super constructor...
        super(p);
    }
    
    /**
     * Adding our custom property
     */
    @Override
    protected void postinit() {
        try {
            // where this file is, we also 
            // perform a check if we can write the file...
            this.url = this.get("url");
            this.f = new File(this.url);
            //if (!f.canWrite()) {
            //    throw new Exception("Cannot write " + this.url);
             //}
            
            // no need to rotate for the moment...
            this.rotate = false;
            this.checkRotationInterval = 10000;

            rotationtype: {
	            // determine rotation type with default to size if null
	            if (this.get("rotation") != null) {
	                rotation = Rotation.valueOf(
	                        (this.get("rotation")).toUpperCase());
	
	                // loading different property depending on rotation type
	                switch (rotation) {
	                    // rotation by size
		                case SIZE:
		                    // maximum size before rotating
		                    if (this.get("size") != null) {
		                        this.sizeLimit = Long.parseLong(this.get("size"));
		                    } else {
		                        this.sizeLimit = DEFAULT_SIZE;
		                    }
		                    break;
		                // rotation by date
	                    case DATE:
	                        // maximum size before rotating
	                        if (this.get("when") != null) {
	                            this.when = When.valueOf(this.get("when").toUpperCase());
	                        } else {
	                            this.when = DEFAULT_ROTATION_DATE;
	                        }	                        
	                        break;
	                        
	                    case NONE:
	                    	break rotationtype;
	                }
	
	            } else {
	                // setting default rotation to size
	                // with default value
	                this.rotation = Rotation.SIZE;
	                this.sizeLimit = DEFAULT_SIZE;
	            }
            }
            
            // if rotation type is NONE there's no need
            // to do anything otherwise ...
            // there's a lot of things to do
            if (rotation != Rotation.NONE) { 
            
	            // little thread for checking if we need to rotate
	            // our file, it is declared as inner class because it is going to modify 
	            // the status and some other private/protected variable of this dumper
	            Runnable r = new Runnable() {
	                
	                // implementing the run interface
	                // as requested by the Runnable interface
	                public void run() {
	                	
	                	// first time checker...
	                	long lastModified = f.lastModified();
	                	
	                    checker:
	                    while (true) {
	                        try {
	                            // check rotation type
	                            switch (rotation) {
	                            
	                                // rotation type by size...
	                                case SIZE:
	                                    // we first check the size
	                                    if (f.exists() && f.length() > sizeLimit) {
	                                        rotate = true;
	                                    }
	    	                            // just pausing....
	    	                            Thread.sleep(checkRotationInterval);
	                                    break;
	                                    
	                                // rotation by date...
	                                case DATE:
	                                	// then we calculate when we will wakeup..and work
	                                	// take current moment as Calendar
	                                	Calendar c = Calendar.getInstance();

	                                	switch(when) {
	                                		// every hour...
	                                		case HOURLY:	                                			
	                                			// add +1 hour to calendar
	                                			// and set minute to 0
	                                			c.add(Calendar.HOUR, +1);
	                                			c.set(Calendar.MINUTE, 0);
	                                			break;

	                                		// every day...
	                                		case DAILY:
	                                			// add +1 day
	                                			c.add(Calendar.DAY_OF_YEAR, +1);
	                                			// set other parameters...
	                                			c.set(Calendar.HOUR,1);
	                                			c.set(Calendar.MINUTE,0);
	                                			break;

	                                		// every week...
	                                		case WEEKLY:
	                                			// add +1 day
	                                			c.add(Calendar.WEEK_OF_YEAR, +1);
	                                			// set other parameters...
	                                			c.set(Calendar.DAY_OF_WEEK, 0);
	                                			c.set(Calendar.HOUR,0);
	                                			c.set(Calendar.MINUTE,0);
	                                			break;

	                                		// every month...
	                                		case MONTHLY:
	                                			// add +1 day
	                                			c.add(Calendar.MONTH, +1);
	                                			// set other parameters...
	                                			c.set(Calendar.DAY_OF_MONTH, 0);
	                                			c.set(Calendar.HOUR,0);
	                                			c.set(Calendar.MINUTE,0);
	                                			break;

	                                	}
	    	                            // just pausing....
	                                	checkRotationInterval = c.getTimeInMillis() - System.currentTimeMillis();
	                                	                    				    
	                                	// just a default value...
	                                	if (checkRotationInterval < 180000)
	                                		checkRotationInterval = 180000;
	                                	
	                                	Thread.sleep(checkRotationInterval);
                       				    // then we can safely rotate...
                       				    rotate = true;
	                                	break;
	                                	
	                                // no rotation...
	                                case NONE:
	                                	// just in case we run the thread 
	                                	// by mistake
	                                	break checker;
	                            }

	                        } catch (java.lang.InterruptedException e) {
	                            // if we get an error the FileDumper
	                            // must stop...
	                            status = Status.FAILURE;
	                            Log.serviceLog(e);
	                        }
	                    }
	                }
	            };
           
            	// starting the checker...
            	Thread t = new Thread(r);
            	t.setName("RotationChecker");
            	// this thread cannot block 
            	// a whole application
            	t.setDaemon(true);
            	t.start();
            }

        } catch (Exception e) {
            throw new ObjectInitializationException(e);
        }
    }
    
    
    synchronized void setRotate(boolean rotate) {
    	this.rotate = rotate;
    	if (rotate) {    		
    		try {
				wait();
			} catch (InterruptedException e) {
				Log.serviceLog(e);
			}
    	}    	
    }

    /**
     * Open this file and 
     * @throws DumperException
     */
    @Override
    protected void open() throws DumperException {
        try {
            //ch = new RandomAccessFile(f, "rw").getChannel();
            FileOutputStream fos = new FileOutputStream(f,true);
            ch = fos.getChannel();
            //bw = new BufferedWriter(Channels.newWriter(ch, Constant.DEFAULT_ENCODING));
            synchronized(this){
            	bw = new BufferedWriter(Channels.newWriter(ch, Constant.DEFAULT_ENCODING));
            }            
            // acquire lock
            FileLock lock = ch.tryLock();
                        
        } catch (IOException e) {
            throw new DumperException(e);
        }
    }

    /**
     * 
     * @param e Anything extending Entry
     * @throws DumperException
     */
    @Override
    protected void write(Entry e) throws DumperException {
        try {
        	
            // dumping entry to file
            bw.append(e.getLine());
            bw.flush();

            if (rotate)
            	this.rotateLog();
            
        } catch (IOException ex) {
            throw new DumperException(ex);
        }
    }

    /**
     * Flush and close the BufferedWriter used to write to file
     */
    @Override
    protected void close() throws DumperException {
        try {
            // basic closing...
            // but we first flush it...
            bw.flush();
            bw.close();

        } catch (IOException e) {
            throw new DumperException(e);
        }
    }

    /**
     * Rotate our log file
     * @throws IOException If something went wrong rotating file
     */
    protected synchronized final void rotateLog() throws IOException, DumperException {
        // don't be scared by this, final result will be: filename.log20080610180512
        String oldLog = url + new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());

        // first we close the file
        bw.flush();
        bw.close();
        // then it's time to rename file to a new destination
        // and check if it went fine
        if (!f.renameTo(new File(oldLog))) {
            throw new IOException("Couldn't do rotation by size");
        }
        //Log.serviceLog(t.getName());
        
        // then we re-open the writer
        this.open();

        //this.isOpen=false;
        // if keep alive true we need to reopen the file
        // otherwise this is done normally in postprepare()
        //if (this.isKeepAlive()) {
        //	this.open();
        //}
        
        // set to default again...
        this.rotate = false;
        // release RotationChecker...
        notifyAll();
    }
}




